Effective Java 2.0_中文版__Item 1

文章作者:Tyan
博客:noahsnail.com

第二章

这章是关于创建和销毁对象的:什么时候怎样创建它们,什么时候怎样避免创建它们,怎样确保它们被及时的销毁,怎么管理任何清理操作,清理操作必须在对象销毁之前。

Item 1: 考虑用静态工厂方法代替构造函数

一个类允许客户获得它本身的一个实例通常的方式是提供一个公有的构造函数。还有另一种技术应该成为每个程序员工具箱中的一部分。一个类可以提供一种公有的static factory methodstatic factory method是一种简单的静态方法,它会返回一个类的实例。这有一个来自Boolean(基本类型boolean的封装类)的简单例子。这个方法将一个布尔值转成Boolean对象的引用:

1
2
3
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}

注意静态工厂方法与Design Patterns中的Factory Method是不同的。这个条目中描述的静态工厂方法与设计模式中的工厂方法是不等价的。

一个类可以为它的客户提供静态工厂方法来代替构造函数,或者除了构造函数之外再提供一个静态工厂方法。提供静态工厂方法代替公有构造函数既有优点也有缺点。

与构造函数相比,静态工厂方法的第一个优势是它们有名字。如果构造函数的参数本身不能描述返回的对象,具有合适名字的静态工厂是更容易使用的,并且产生的客户端代码更易读。例如,构造函数BigInteger(int, int, Random)返回一个BigInteger,这个BigInteger可能是一个素数,使用名字为BigInteger.probablePrime的静态工厂方法来表示会更好。(这个方法最终在1.4版本被引入。)

一个类只能有一个具有指定签名的构造函数。程序员知道怎样规避这个限制:通过提供两个构造函数,它们仅在参数列表类型的顺序上有所不同。这真的是一个坏主意。使用这种API的用户永远不能记住哪一个构造函数是哪一个,最后会无意中调用错误的构造函数。使用这些构造函数的人在读代码时如果没有类的参考文档将不知道代码要做什么。

因为静态工厂方法有名字,因此它们不会有上一段讨论的那种限制。当一个类似乎需要多个具有相同签名的构造函数时,用静态工厂方法代替构造函数,通过仔细选择工厂方法的名字来突出它们的不同。

与构造函数相比,静态工厂方法的第二个优势是当调用静态工厂方法时不要求每次都创建一个新的对象。这允许不可变类(Item 15)使用预创建的实例,或缓存构建好的实例,通过重复分发它们避免创建不必要的重复对象。Boolean.valueOf(boolean)方法阐明了这个技术:它从未创建对象。这项技术与Flyweight模式类似[Gamma95, p. 195]。如果经常请求相同的对象,它能极大的提升性能,尤其是在创建对象的代价较昂贵时。

静态工厂方法能从重复的调用中返回相同的对象,在任何时候都能使类严格控制存在的实例。这些类被称为控制实例。编写控制实例类是有一些原因的。实例控制允许一个类保证它是一个单例(Item 3)或不可实例化的(Item 4)。它也允许一个不变的类(Item 15)保证不存在两个相等的实例:a.equals(b)当且仅当a==b。如果一个类保证了这一点,它的客户端可以使用==操作符代替equals(Object)方法,这可能会导致性能的提升。Enum类型(Item 30)保证了这一点。

与构造函数相比,静态工厂方法的第三个优势是它们能返回它们的返回类型的任意子类型的对象。这样在选择返回对象的类时有了更大的灵活性。

灵活性的一个应用是API能返回对象而不必使它们的类变成公有的。通过这种方式中隐藏实现类会有一个更简洁的API。这项技术适用于基于接口的框架(Item 18),接口为静态工厂方法提供了自然的返回类型。接口不能有静态方法,因此按惯例,命名为Type的接口的静态工厂方法被放在一个命名为Types的不可实例化的类中(Item 4)。

例如,Java集合框架有三十二个集合接口的便利实现,提供了不可修改的集合,同步集合等等。几乎所有的这些实现都是通过静态工厂方法导出在一个不可实例化的类中(java.util.Collections)。返回对象的类都是非公有的。

集合框架API比它导出的三十二个分开的公有类更小,每一个便利实现对应一个类。它不仅仅是API的数量在减少,还是概念上意义上的减少。用户知道返回的对象含有接口指定的精确API,因此不需要阅读额外的实现类的文档。此外,使用这样的静态工厂方法需要客户端使用接口引用返回的对象而不是使用它的实现类,这通常是最佳的实践(Item 52)。

不仅公有静态工厂方法返回对象的类可以是非公有的,而且这个类还可以随着调用静态工厂时输入的参数值的变化而变化。声明的返回值类型的任何子类都是可以的。为了增强软件的可维护性及性能,返回值对象的类也可以随着发布版本的变化而变化。

在1.5版本中引入类java.util.EnumSet(Item 32),它没有公有的构造函数,只有静态工厂方法。根据枚举类型的大小,静态工厂方法返回两个实现中的一个,枚举类型的分类:如果枚举类型中有六十四个元素或更少,与大多数枚举类型一样,静态工厂返回一个RegularEnumSet实例,由单个的long支持;如果枚举类型中有六十五个元素或更多,静态工厂方法返回一个JumboEnumSet实例,由long[]支持。

现有的两个实现类对于客户端是不可见的。如果RegularEnumSet对于较少数量的枚举类型没有提供性能优势,那么在将来的版本中将其移除不会任何影响。同样地,如果新的EnumSet实现在性能上更有优势,在将来的版本中添加EnumSet的第三或第四个实现也不会有任何影响。客户端不知道也不关心它们从工厂方法中得到的对象所属的类;它们只关心它是EnumSet的某个子类。

在编写静态工厂方法所属的类时,静态工厂方法返回的对象所属的类可以不必存在。这种灵活的静态工厂方法形成了服务提供者框架的基础,例如Java数据库链接API(JDBC)。服务提供者框架是一个系统:多个服务提供者实现一个服务,系统为客户端提供服务的多个实现,使客户端与服务实现解耦。

服务提供者框架有三个基本的组件:服务接口,提供者实现;提供者注册API,系统用来注册实现,使客户端能访问它们;服务访问API,客户端用来得到服务实例。服务访问API通常允许但不要求客户端指定一些选择提供者的规则。在没有指定的情况下,API返回一个默认的实现实例。服务访问API是”灵活的静态工厂”,其形成了服务提供者框架的基础。

服务提供者框架的第四个可选组件是服务提供者接口,服务提供者通过实现这个接口来创建服务实现的实例。在没有服务提供者接口的情况下,服务实现通过类名进行注册,通过反射来进行实例化(Item 53)。在JDBC的案例中,Connection是服务接口,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection服务访问API,Driver是服务提供者接口。

服务提供者框架模式有许多变种。例如,服务访问API通过使用适配器模式[Gamma95, p. 139],能返回比提供者需要的更更丰富的服务接口。下面是服务提供者接口的一个简单实现和默认的提供者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Service provider framework sketch

// Service interface
public interface Service {
... // Service-specific methods go here
}

// Service provider interface
public interface Provider {
Service newService();
}

// Noninstantiable class for service registration and access
public class Services {
private Services() { } // Prevents instantiation (Item 4)

// Maps service names to services
private static final Map<String, Provider> providers =
new ConcurrentHashMap<String, Provider>();

public static final String DEFAULT_PROVIDER_NAME = "<def>";

// Provider registration API
public static void registerDefaultProvider(Provider p) {
registerProvider(DEFAULT_PROVIDER_NAME, p);
}

public static void registerProvider(String name, Provider p){
providers.put(name, p);
}

// Service access API
public static Service newInstance() {
return newInstance(DEFAULT_PROVIDER_NAME);
}

public static Service newInstance(String name) {
Provider p = providers.get(name);
if (p == null)
throw new IllegalArgumentException(
"No provider registered with name: " + name);
return p.newService();
}
}

静态工厂方法的第四个优势是它们降低了创建参数化类型实例的冗长性。遗憾的是,当你调用参数化类的构造函数时,你必须指定类型参数,即使它们在上下文中是非常明显的。这通常需要你紧接着提供两次类型参数:

1
2
Map<String, List<String>> m =
new HashMap<String, List<String>>();

随着类型参数长度和复杂性的增加,这个冗长的说明很快就让人变得很痛苦。但是使用静态工厂的话,编译器可以为你找出类型参数。这被称为类型推导。例如,假设HashMap由这个静态工厂提供:

1
2
3
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}

你可以将上面冗长的声明用下面简洁的形式去替换:

1
Map<String, List<String>> m = HashMap.newInstance();

某一天,Java语言可能在构造函数调用上也有与方法调用类似的类型推导,但到发行版本1.6为止,它一直没有。

遗憾的是,但到发行版本1.6为止,标准集合实现例如HashMap没有工厂方法,但你可以把这些方法放到你自己的工具类力。更重要的是,你可以在你自己的参数化类里提供这样的静态工厂。

只提供静态工厂方法的缺点是没有公有或保护构造函数的类不能进行子类化。公有静态工厂返回的非公有类同样如此。例如,不可能子类化集合框架中的这些便利实现类。可以说这是因祸得福,因为它鼓励程序员使用组合来代替继承(Item 16)。

静态工厂方法的第二个缺点是它们不能很容易的与其它静态方法进行区分。它们不能像构造函数那样在API文档中明确标识出来,因此很难弄明白怎样实例化一个提供静态工厂方法代替构造函数的类。Javadoc工具可能某一天会关注静态工厂方法。同时,你可以通过在类中或接口注释中注意静态工厂和遵循通用命名约定来减少这个劣势。下面是静态工厂方法的一些常用命名:

  • valueOf — 不严格地说,返回一个与它的参数值相同的一个实例。这种静态工厂是有效的类型转换方法。

  • ofvalueOf的一种简洁替代方法,通过EnumSet(Item 32)得到普及。

  • getInstance — 返回一个通过参数描述的实例,但不能说是相同的值。在单例情况下,getInstance没有参数并且返回唯一的一个实例。

  • newInstance — 除了newInstance保证每个返回的实例都是与其它的实例不同之外,其它的类似于getInstance

  • getType — 类似于getInstance,当静态工厂方法在不同的类中时使用。Type表示静态工厂方法返回的对象类型。

  • newType — 类似于newInstance,当静态工厂方法在不同的类中时使用。Type表示静态工厂方法返回的对象类型。

总之,静态工厂方法和公有构造函数都有它们的作用,理解它们的相对优势是值得的。静态工厂经常是更合适的,因此要避免习惯性的提供公有构造函数而不首先考虑静态工厂。

如果有收获,可以请我喝杯咖啡!